/**
 * The server for caro game
 */
package pq.net;

import java.awt.BorderLayout;
import java.awt.Point;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Formatter;
import java.util.Scanner;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

import ch.randelshofer.quaqua.util.Worker;

import pq.util.AppConfigReader;

/**
 * @author tqthe
 * 
 */
public class CaroServer extends JFrame {

	private String[][] board; // caro board
	private int boardRow;
	private int boardColumn;
	private JTextArea outputArea;
	private final static int PLAYER_X = 0;
	private final static int PLAYER_O = 1;
	private final static String[] MARKS = { "X", "O" };
	private int currentPlayer;
	private Player[] players;
	private ServerSocket server; // server socket to connect with clients
	private int port; // the port server will listen
	private ExecutorService runGame; // will run player
	private Lock gameLock; // to lock game for synchronization
	private Condition otherPlayerConnected; // to wait for other player
	private Condition otherPlayerTurned; // to wait for other player's turn

	// use the boundary rectangle instead of entire board to optimize checking
	// game over
	private Point topLeftX = new Point();
	private Point rightBottomX = new Point();
	private Point topLeftO = new Point();
	private Point rightBottomO = new Point();

	public CaroServer() {

		super("Caro Server");

		loadServerSettings("server-config.xml");
		setLookFeel();

		// create ExecutorService with a thread for each player
		runGame = Executors.newFixedThreadPool(2);
		gameLock = new ReentrantLock();

		// condition variable for both players being connected
		otherPlayerConnected = gameLock.newCondition();

		// condition variable for the other player's turn
		otherPlayerTurned = gameLock.newCondition();

		createCaroBoard();

		players = new Player[2];
		currentPlayer = PLAYER_X; // set player X as the first player

		// set up server socket
		try {
			server = new ServerSocket(port, 2);
		} catch (IOException ex) {
			ex.printStackTrace();
			System.exit(1);
		}

		// initialize boundary rectangle
		topLeftX.setLocation(boardRow, boardColumn);
		rightBottomX.setLocation(0, 0);
		topLeftO.setLocation(boardRow, boardColumn);
		rightBottomO.setLocation(0, 0);

		outputArea = new JTextArea();
		add(new JScrollPane(outputArea), BorderLayout.CENTER);
		outputArea.setText("Server awaiting connections\n");

		setSize(400, 300);
		setVisible(true);
		pack();
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	}

	/**
	 * Wait for two connections so game can be played
	 */
	public void execute() {

		// wait for each client to connect
		for (int i = 0; i < players.length; i++) {
			try {
				// wait for connection, create Player, start runnable
				players[i] = new Player(server.accept(), i);
				runGame.execute(players[i]);
			} catch (IOException ex) {
				ex.printStackTrace();
				System.exit(1);
			}
		}

		// lock game to signal player X's thread
		gameLock.lock();

		try {
			players[PLAYER_X].setSuspend(false); // resume player X
			otherPlayerConnected.signal(); // wake up player X's thread
		} finally {
			// unlock game after signalling player X
			gameLock.unlock();
		}
	}

	/**
	 * Determine if move is valid
	 */
	private boolean validateAndMove(int player, int row, int column) {

		// while not current player, must wait for turn
		while (player != currentPlayer) {

			// lock game to wait for other player to go
			gameLock.lock();

			try {
				// wait for player's turn
				otherPlayerTurned.await();
			} catch (InterruptedException e) {
				e.printStackTrace();
			} finally {
				// unlock game after waiting
				gameLock.unlock();
			}
		}

		// if location not occupied, make move
		if (!isOccupied(row, column)) {

			updateBoundary(row, column);

			board[row][column] = MARKS[currentPlayer]; // set move on board
			currentPlayer = (currentPlayer + 1) % 2; // change player

			// let new current player know that move occurred
			players[currentPlayer].otherPlayerMoved(row, column);

			// lock game to signal other player to go
			gameLock.lock();

			try {
				otherPlayerTurned.signal(); // signal other player to continue
			} finally {
				gameLock.unlock(); // unlock game after signaling
			}

			return true; // notify player that move was valid
		}

		return false; // notify player that move was invalid
	}

	/**
	 * Update boundary for Player X and O
	 * 
	 * @param row
	 * @param column
	 */
	private void updateBoundary(int row, int column) {

		// update boundary rectangle
		if (currentPlayer == 0) {
			// update X boundary
			if (row < topLeftX.x) {
				topLeftX.x = row;
			}
			if (column < topLeftX.y) {
				topLeftX.y = column;
			}
			if (row > rightBottomX.x) {
				rightBottomX.x = row;
			}
			if (column > rightBottomX.y) {
				rightBottomX.y = column;
			}
		} else {
			// update O boundary
			if (row < topLeftO.x) {
				topLeftX.x = row;
			}
			if (column < topLeftO.y) {
				topLeftX.y = column;
			}
			if (row > rightBottomO.x) {
				rightBottomX.x = row;
			}
			if (column > rightBottomO.y) {
				rightBottomX.y = column;
			}
		}
	}

	/**
	 * Determine whether location is occupied
	 * 
	 * @param row
	 *            - The row index (base-zero) of the location
	 * @param column
	 *            - The column index (base-xero) of the location
	 * @return If the location is occupied return true. False otherwise
	 */
	public boolean isOccupied(int row, int column) {

		boolean x = board[row][column].equals(MARKS[PLAYER_X]);
		boolean o = board[row][column].equals(MARKS[PLAYER_O]);

		if (x || o) {
			return true;
		}
		return false; // the location is free (available)
	}

	/**
	 * Determine whether game over
	 * 
	 * @return True if game is over. False otherwise
	 */
	public boolean isGameOver() {

		if (gameOverHelper(PLAYER_X)) {
			return true;
		}
		if (gameOverHelper(PLAYER_O)) {
			return true;
		}
		return false;
	}

	/**
	 * 
	 * @param player
	 * @return
	 */
	private boolean gameOverHelper(int player) {

		// walk through rows
		int x1 = 0;//(player == 0) ? topLeftX.x : topLeftO.x;
		int y1 = 0;//(player == 0) ? topLeftX.y : topLeftO.y;
		int x2 = boardRow - 1;//(player == 0) ? rightBottomX.x : rightBottomO.x;
		int y2 = boardColumn - 1;//(player == 0) ? rightBottomX.y : rightBottomO.y;

		for (int i = x1; i <= x2; i++) {
			for (int j = y1; j <= y2; j++) {
				if (checkRow(player, i, j)) {
					return true;
				}
				if (checkColumn(player, i, j)) {
					return true;
				}
				if (checkBackDiagonal(player, i, j)) {
					return true;
				}
				if (checkForwardDiagonal(player, i, j)) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * 
	 * @param player
	 * @param row
	 * @param column
	 * @return
	 */
	private boolean checkRow(int player, int row, int column) {

		if (!board[row][column].equals(MARKS[player]))
			return false;

		for (int j = 1; j <= 4; j++) {
			int nextColumn = column + j;
			if (nextColumn < boardColumn) {
				if (!board[row][nextColumn].equals(MARKS[player])) {
					return false;
				}
			} else {
				return false;
			}
		}
		return true;
	}

	/**
	 * 
	 * @param player
	 * @param row
	 * @param column
	 * @return
	 */
	private boolean checkColumn(int player, int row, int column) {

		if (!board[row][column].equals(MARKS[player]))
			return false;

		for (int i = 1; i <= 4; i++) {
			int nextRow = row + i;
			if (nextRow < boardRow) {
				if (!board[nextRow][column].equals(MARKS[player])) {
					return false;
				}
			} else {
				return false;
			}
		}

		return true;
	}

	/**
	 * 
	 * @param player
	 * @param row
	 * @param column
	 * @return
	 */
	private boolean checkBackDiagonal(int player, int row, int column) {

		if (!board[row][column].equals(MARKS[player]))
			return false;

		for (int k = 1; k <= 4; k++) {
			int nextRow = row + k;
			int nextColumn = column + k;
			if ((nextRow < boardRow) && (nextColumn < boardColumn)) {
				if (!board[nextRow][nextColumn].equals(MARKS[player])) {
					return false;
				}
			} else {
				return false;
			}
		}
		return true;
	}

	/**
	 * 
	 * @param player
	 * @param row
	 * @param column
	 * @return
	 */
	private boolean checkForwardDiagonal(int player, int row, int column) {

		if (!board[row][column].equals(MARKS[player]))
			return false;

		for (int k = 1; k <= 4; k++) {
			int nextRow = row + k;
			int nextColumn = column - k;
			if ((nextRow < boardRow) && (nextColumn >= 0)) {
				if (!board[nextRow][nextColumn].equals(MARKS[player])) {
					return false;
				}
			} else {
				return false;
			}
		}
		return true;
	}

	/**
	 * Private inner class Player manages each Player as a runnable
	 * 
	 * @author tqthe
	 * 
	 */
	private class Player implements Runnable {

		private Socket connection; // connection to client
		private Scanner input; // input from client
		private Formatter output; // output to client
		private int playerNumber; // keep track which player this is
		private String mark; // mark (caro symbol) for this player
		private boolean isSuspended = true; // whether thread is suspended

		/**
		 * Set up player thread
		 * 
		 * @param socket
		 * @param number
		 */
		public Player(Socket socket, int number) {

			playerNumber = number;
			mark = MARKS[number];
			connection = socket; // store socket for client

			// obtains streams from socket
			getStreams();
		}

		/**
		 * Control thread's execution
		 */
		@Override
		public void run() {

			// send client its mark (X or O), process messages from client
			try {
				displayMessage("Player " + mark + " connected\n");
				output.format("%s\n", mark); // send player's mark
				output.flush();

				// if player is X, wait for another player to arrive
				if (playerNumber == PLAYER_X) {
					output.format("%s\n%s", "Player X connected",
							"Waiting for another player\n");
					output.flush();

					// lock game to wait for second player
					gameLock.lock();

					try {
						while (isSuspended) {
							otherPlayerConnected.await(); // wait for player O
						}
					} catch (InterruptedException ex) {
						ex.printStackTrace();
					} finally {
						gameLock.unlock(); // unlock game after second player
						// join
					}

					// send message that other player connected
					output.format("Other player connected. You move.\n");
					output.flush();
				} else {
					output.format("Player O connected. Please wait...\n");
					output.flush();
				}

				// while game not over
				while (!isGameOver()) {

					int row = 0;
					int column = 0;

					if (input.hasNext()) {
						row = input.nextInt();
					}
					if (input.hasNext()) {
						column = input.nextInt();
					}

					// check for valid move
					if (validateAndMove(playerNumber, row, column)) {
						displayMessage(String.format("\nLocation: [%d, %d]",
								row, column));
						output.format("Valid move.\n");
						output.flush();
					} else {
						output.format("Invalid move, try again\n");
						output.flush(); // flush output
					}
				} // end while
				
				output.format("Game over!\n");
				output.flush();
				
			} finally {
				try {
					connection.close();
				} catch (IOException ex) {
					ex.printStackTrace();
					System.exit(1);
				}
			}
		}

		/**
		 * Send message that other player moved
		 * 
		 * @param row
		 *            - The row index of opponent's choice
		 * @param column
		 *            - The column index of opponent's choice
		 */
		public void otherPlayerMoved(int row, int column) {

			output.format("Opponent moved\n");
			output.format("%d %d\n", row, column);
			output.flush();
		}

		/**
		 * Set whether or not thread is suspended
		 * 
		 * @param status
		 *            - The status of the thread
		 */
		public void setSuspend(boolean status) {

			isSuspended = status;
		}

		/**
		 * Obtains streams from socket
		 */
		private void getStreams() {

			try {
				output = new Formatter(connection.getOutputStream());
				input = new Scanner(connection.getInputStream());
			} catch (IOException e) {
				e.printStackTrace();
				System.exit(1);
			}
		}

	} // end class Player

	/**
	 * Display message in outputArea
	 * 
	 * @param message
	 *            - The message to display
	 */
	private void displayMessage(final String message) {

		SwingUtilities.invokeLater(new Runnable() {

			@Override
			public void run() {

				outputArea.append(message);
			}
		});
	}

	/**
	 * Initialize caro board
	 */
	private void createCaroBoard() {

		board = new String[boardRow][boardColumn];

		// create caro board
		for (int i = 0; i < board.length; i++) {
			for (int j = 0; j < board[i].length; j++) {
				board[i][j] = new String("");
			}
		}

		// TODO Create GUI board to monitor game

	}

	/**
	 * Load server configuration from XML file
	 * 
	 * @param configFile
	 *            - The xml file that contains settings
	 */
	private void loadServerSettings(String configFile) {

		// Load application configuration
		AppConfigReader configer = new AppConfigReader(configFile);

		port = Integer.parseInt(configer.getValue("port"));
		boardRow = Integer.parseInt(configer.getValue("boardRows"));
		boardColumn = Integer.parseInt(configer.getValue("boardColumns"));
	}

	/**
	 * Make GUI look like Mac OS X GUI
	 */
	private void setLookFeel() {

		// set system properties here that affect Quaqua
		// for example the default layout policy for tabbed
		// panes:
		System.setProperty("Quaqua.tabLayoutPolicy", "wrap");

		try {
			UIManager
					.setLookAndFeel("ch.randelshofer.quaqua.QuaquaLookAndFeel");
		} catch (ClassNotFoundException e1) {
			e1.printStackTrace();
		} catch (InstantiationException e1) {
			e1.printStackTrace();
		} catch (IllegalAccessException e1) {
			e1.printStackTrace();
		} catch (UnsupportedLookAndFeelException e1) {
			e1.printStackTrace();
		}
	}

}
